package com.creatified.jacqueline;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.xml.xpath.XPathException;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.tidy.Tidy;


/**
 * Your main entry point when using the features of Jacqueline.
 * Jacqueline lets you add a list of CSS files and a list of HTML documents at creation time.
 * It will check the CSS selectors retrieved from the CSS files against the HTML documents and return a JacquelineResult for this.
 */
public class Jacqueline {

	protected final static Logger LOG = Logger.getLogger(Jacqueline.class);

	private final List<File> cssFiles;
	private final List<Document> htmlDocuments;
	private final List<JacquelineSelector> cssSelectors;

	private final Set<Thread> workers;

	private JacquelineResult result;
	private final List<Document> documentsToCheck = new ArrayList<Document>();

	/**
	 * Instantiates a new jacqueline. From a List of Files as CSS files and a List of Documents as HTML documents.
	 * 
	 * @param cssFiles the css files to extract CSS rules/selectors from
	 * @param htmlDocuments the html documents to check against
	 */
	public Jacqueline(List<File> cssFiles, List<Document> htmlDocuments) throws IOException {
		this.cssFiles = cssFiles;
		this.htmlDocuments = htmlDocuments;
		cssSelectors = new ArrayList<JacquelineSelector>();

		workers = new HashSet<Thread>();

		documentsToCheck.addAll(htmlDocuments);

		cssSelectors.addAll(getSelectors(this.cssFiles));
	}

	/**
	 * Instantiates a new jacqueline. From only one File as CSS file and one Document as HTML document.
	 * 
	 * @param cssFile the CSS file
	 * @param doc the document
	 */
	public Jacqueline(File cssFile, Document doc) throws IOException {
		this(Arrays.asList(cssFile), Arrays.asList(doc));
	}

	/**
	 * Instantiates a new jacqueline from a single CSS file and several documents.
	 * 
	 * @param cssFile the CSS file
	 * @param docs the documents
	 */
	public Jacqueline(File cssFile, List<Document> docs) throws IOException {
		this(Arrays.asList(cssFile), docs);
	}

	/**
	 * Processes all the documents and CSS files associated with this Jacqueline instance
	 * 
	 * @return the resulting JacquelineResult
	 */
	public JacquelineResult process() throws IOException, XPathException {
		LOG.info("Checking " + htmlDocuments.size() + " document(s)");
		long start = System.currentTimeMillis();

		generateWorkers();

		while(workers.size() > 0) {

		}
		
		LOG.info("took us: " + (System.currentTimeMillis() - start) + "ms");
		
		return this.result;
	}

	protected synchronized void workerFinished(JacquelineWorker worker, JacquelineResult result) {
		LOG.debug("Worker finished!");
		workers.remove(worker);

		this.result = JacquelineResult.mergeResults(this.result, result);

		if(documentsToCheck.size() > 0) {
			JacquelineWorker w = new JacquelineWorker(this, documentsToCheck.get(0), cssSelectors); 
			workers.add(w);
			documentsToCheck.remove(0);
			w.run();
		}
	}

	/**
	 * Utility method to create a Document from a FileInputStream
	 * 
	 * @param f the FileInputStream to read from
	 * 
	 * @return the generated Document
	 */
	public static Document generateDocument(FileInputStream f) {
		Tidy tidy = new Tidy();

		tidy.setXHTML(false);
		tidy.setQuiet(true);
		tidy.setShowWarnings(false);

		return tidy.parseDOM(f, null);
	}

	/**
	 * Generates a document from a file
	 * 
	 * @param file the file to read from
	 * 
	 * @return the generated document
	 * 
	 * @throws FileNotFoundException if the File could not be found
	 */
	public static Document generateDocument(File file) throws FileNotFoundException {
		return generateDocument(new FileInputStream(file));
	}

	/**
	 * Generates a document from a file referenced by an abstract path.
	 * 
	 * @param filename the path to the file
	 * 
	 * @return the generated document
	 * 
	 * @throws FileNotFoundException if the file could not be found
	 */
	public static Document generateDocument(String filename) throws FileNotFoundException {
		return generateDocument(new FileInputStream(filename));
	}

	/**
	 * Gets the CSS selectors from several files
	 * 
	 * @return the selectors
	 */
	private List<JacquelineSelector> getSelectors(List<File> cssFiles) throws IOException {
		List<JacquelineSelector> selectors = new ArrayList<JacquelineSelector>();

		for(File f: cssFiles) {
			selectors.addAll(getSelectorsFromCss(f));
		}

		return selectors;
	}

	/**
	 * Utility method to retrieve all selectors from a CSS file
	 * 
	 * @param f the file to read from
	 * 
	 * @return the parsed CSS selectors as a List of JacquelineSelector instances
	 */
	private static ArrayList<JacquelineSelector> getSelectorsFromCss(File f) throws IOException {
		JacquelineCSSParser jf = new JacquelineCSSParser(f);

		LOG.debug("found " + jf.getSelectors().size() + " selectors in document '" + f.getPath() + "'");

		return jf.getSelectors();
	}

	private void generateWorkers() {
		generateWorkers(Runtime.getRuntime().availableProcessors());
	}

	private void generateWorkers(int count) {
		for(int i = 0; i < count; i++) {
			if(documentsToCheck.size() > 0) {
				JacquelineWorker w = new JacquelineWorker(this, documentsToCheck.get(0), cssSelectors);
				workers.add(w);
				documentsToCheck.remove(documentsToCheck.get(0));
				w.run();
			}
		}
	}


}
